﻿using Microsoft.WindowsAzure.Storage;
using Microsoft.WindowsAzure.Storage.Auth;
using Microsoft.WindowsAzure.Storage.Blob;
using Microsoft.WindowsAzure.Storage.Table;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace AzureStorageLight
{
    public class StorageItemManager
    {
        CloudTable itemsTable;
        CloudBlobContainer fileContainer;
        CloudBlobContainer backupContainer;

        static StorageCacheManager cache = new StorageCacheManager();

        static List<Task> asyncTasks = new List<Task>();

        private string rootId = "0000000000000000";

        public StorageItemManager(string connectionString, string tableName, string containerName, string backupContainerName)
        {
            var storageAccount = CloudStorageAccount.Parse(connectionString);
            var tableClient = storageAccount.CreateCloudTableClient();
            itemsTable = tableClient.GetTableReference(tableName);
            var blobClient = storageAccount.CreateCloudBlobClient();
            fileContainer = blobClient.GetContainerReference(containerName);
            if (backupContainerName != null) backupContainer = blobClient.GetContainerReference(backupContainerName);

            itemsTable.CreateIfNotExists();
            fileContainer.CreateIfNotExists();
            if (backupContainer != null) backupContainer.CreateIfNotExists();
        }

        public StorageItemManager(string accountname, string tableName, string tableSas, string fileContainerName, string fileSas, string backupContainerName = null, string backupSas = null)
        {
            var tblCred = new StorageCredentials(tableSas);
            var tblUri = new Uri(string.Format("https://{0}.table.core.windows.net/{1}", accountname, tableName));
            itemsTable = new CloudTable(tblUri, tblCred);

            var fileCred = new StorageCredentials(fileSas);
            var fileUri = new Uri(string.Format("https://{0}.blob.core.windows.net/{1}", accountname, fileContainerName));
            fileContainer = new CloudBlobContainer(fileUri, fileCred);

            if (backupContainerName != null)
            {
                var backupCred = new StorageCredentials(backupSas);
                var backupUri = new Uri(string.Format("https://{0}.blob.core.windows.net/{1}", accountname, backupContainerName));
                backupContainer = new CloudBlobContainer(backupUri, backupCred);
            }
        }

        public static string ApplicationFolder
        {
            get { return System.IO.Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "azurestoragelight"); }
        }

        private static string GetAccountName(string connectionString)
        {
            var storageAccount = CloudStorageAccount.Parse(connectionString);
            return storageAccount.Credentials.AccountName;
        }

        public static string CreateTicket(string connectionString, string tableName, string containerName, string backupContainerName, DateTime expired, string password)
        {
            var account = GetAccountName(connectionString);
            var tokens = CreateSAS(connectionString, tableName, containerName, backupContainerName, expired);
            var ticket = string.Join("/", account, tableName, tokens[0], containerName, tokens[1], backupContainerName, tokens[2]);
            if (password != null)
            {
                ticket = CryptUtil.EncryptStringAES(ticket, password);
            }
            return ticket;
        }

        public static StorageItemManager CreateStorageItemManager(StorageConfig config)
        {
            if (config.ConnectionString == null)
            {
                string[] works;
                works = config.Ticket.Split('/');
                return new StorageItemManager(works[0], works[1], works[2], works[3], works[4], works[5], works[6]);
            }
            else
            {
                return new StorageItemManager(
                    config.ConnectionString,
                    config.ItemTableName,
                    config.FileBlobContainer,
                    config.BackupBlobContainer);
            }
        }

        public static string[] CreateSAS(string connectionString, string tableName, string containerName, string backupContainerName, DateTime expired)
        {
            var storageAccount = CloudStorageAccount.Parse(connectionString);
            var tableClient = storageAccount.CreateCloudTableClient();
            var itemsTable = tableClient.GetTableReference(tableName);
            var blobClient = storageAccount.CreateCloudBlobClient();
            var fileContainer = blobClient.GetContainerReference(containerName);
            var backupContainer = blobClient.GetContainerReference(backupContainerName);

            itemsTable.CreateIfNotExists();
            fileContainer.CreateIfNotExists();
            if (backupContainer != null) backupContainer.CreateIfNotExists();


            string gcPolicySignedIdentifier = "GCAccessPolicy" + DateTime.UtcNow.ToString();

            SharedAccessTablePolicy gcTablePolicy = new SharedAccessTablePolicy()
            {
                SharedAccessExpiryTime = expired,
                Permissions = SharedAccessTablePermissions.Query | SharedAccessTablePermissions.Add | SharedAccessTablePermissions.Delete | SharedAccessTablePermissions.Update 
            };

            TablePermissions tblPermissions = new TablePermissions();
            tblPermissions.SharedAccessPolicies.Add(
                   gcPolicySignedIdentifier,
                   gcTablePolicy);
            itemsTable.SetPermissions(tblPermissions);

            var tableSasToken = itemsTable.GetSharedAccessSignature(
                new SharedAccessTablePolicy(),
                gcPolicySignedIdentifier,
                null /* start partition key */,
                null /* start row key */,
                null /* end partition key */,
                null /* end row key */);

            SharedAccessBlobPolicy filePolicy = new SharedAccessBlobPolicy
            {
                SharedAccessExpiryTime = expired,
                Permissions = SharedAccessBlobPermissions.Delete | SharedAccessBlobPermissions.List | SharedAccessBlobPermissions.Read | SharedAccessBlobPermissions.Write
            };

            string fileToken = fileContainer.GetSharedAccessSignature(
                filePolicy,
                null /* access policy identifier */);

            SharedAccessBlobPolicy backupPolicy = new SharedAccessBlobPolicy
            {
                SharedAccessExpiryTime = expired,
                Permissions = SharedAccessBlobPermissions.Delete | SharedAccessBlobPermissions.List | SharedAccessBlobPermissions.Read | SharedAccessBlobPermissions.Write
            };

            string backupToken = backupContainer.GetSharedAccessSignature(
                backupPolicy,
                null /* access policy identifier */);

            return new string[] { tableSasToken, fileToken, backupToken };
        }


        private string GetCahcedFilePath(string id)
        {
            return Path.Combine(ApplicationFolder, id);
        }

        private List<StorageItem> GetItemsByFolderId(string id)
        {
            var data = cache.GetCachedData(id);
            if (data != null) return data;

            Debug.WriteLine("*** Loading(folder id) " + id);

            var filter = TableQuery.GenerateFilterCondition("PartitionKey", QueryComparisons.Equal, id);
            var query = new TableQuery<StorageItem>();
            var list = itemsTable.ExecuteQuery<StorageItem>(query.Where(filter)).ToList();
            cache.SetCachedData(id, list);
            return list;
        }

        private StorageItem FindItemById(string id)
        {
            var data = cache.FindCachedItem(id);
            if (data != null) return data;

            Debug.WriteLine("*** Loading(id) " + id);

            var filter = TableQuery.GenerateFilterCondition("RowKey", QueryComparisons.Equal, id);
            var query = new TableQuery<StorageItem>();
            var list = itemsTable.ExecuteQuery<StorageItem>(query.Where(filter)).ToList();
            var result = list.FirstOrDefault();
            if (result != null) GetItemsByFolderId(result.FolderId); // cache set
            return result;
        }

        private StorageItem GetItemById(string id, string childId)
        {
            var list = GetItemsByFolderId(id);
            return list.Where(x => x.Id == childId).FirstOrDefault();
        }

        private StorageItem GetItemByName(string id, string name)
        {
            var list = GetItemsByFolderId(id);
            return list.Where(x => x.Name == name).FirstOrDefault();
        }


        public StorageItem GetItem(string path)
        {
            if (string.IsNullOrEmpty(path) || path == "/")
            {
                return GetItemById("", rootId);
            }

            var parent = StorageItemUtil.GetParent(path);
            var fileName = StorageItemUtil.GetFileName(path);
            if (parent != null)
            {
                var parentItem = GetItem(parent);
                if (parentItem == null) return null;
                return GetItemByName(parentItem.Id, fileName);
            }
            else
            {
                return GetItemByName("", fileName);
            }

        }

        public List<StorageItem> GetFiles(string path, bool reload = false)
        {
            var dire = GetItem(path);
            if (dire == null) throw CreateNotFoundException(path);
            if (reload) cache.ClearCachedData(dire.Id);
            var ret = GetItemsByFolderId(dire.Id);
            return ret;
        }

        public async Task<StorageItem> EnsureItem(string path, bool isHide = false)
        {
            Debug.WriteLine("*** EnsureItem " + path);
            var sw = Stopwatch.StartNew();
            StorageItem direItem;
            var fileName = StorageItemUtil.GetFileName(path);
            var parent = StorageItemUtil.GetParent(path);
            if (string.IsNullOrEmpty(path) || path == "/")
            {
                var item = GetItemById("", rootId);
                if (item != null) return item;
                direItem = new StorageItem("", rootId);

            }
            else
            {
                var parentItem = GetItem(parent);
                if (parentItem == null) throw CreateNotFoundException(parent);
                var item = GetItemByName(parentItem.Id, fileName);
                if (item != null) return item;
                direItem = new StorageItem(parentItem.Id, Guid.NewGuid().ToString("N"));
            }
            direItem.Name = fileName;
            direItem.Size = 0;
            direItem.Created = DateTime.UtcNow;
            direItem.LastModified = DateTime.UtcNow;
            direItem.Owner = GetCurretUser();
            direItem.Type = GetDataType(path);
            direItem.IsHide = isHide;
            var operation = TableOperation.Insert(direItem);
            await itemsTable.ExecuteAsync(operation);
            cache.AddCachedData(direItem);
            Debug.WriteLine("### Ensure " + sw.Elapsed.ToString());
            return direItem;

        }

        public string GetPath(StorageItem item)
        {
            if (item == null) return "";
            var parent = FindItemById(item.FolderId);
            if (parent == null) return "";
            return GetPath(parent) + "/" + item.Name;
        }
        public string GetRelativePath(StorageItem item)
        {
            return GetPath(item).TrimStart('/');
        }

        private string GetDataType(string path)
        {
            return path.EndsWith("/") ? "Directory" : "File";
        }
        public async Task UploadAsync(string path, string filePath)
        {
            Debug.WriteLine("*** Uplaod " + path);
            var sw = Stopwatch.StartNew();
            var item = await EnsureItem(path);
            var fi = new System.IO.FileInfo(filePath);
            item.Size = fi.Length;
            item.LastModified = fi.LastWriteTimeUtc;
            item.Owner = GetCurretUser();
            item.HasBackup = false;
            var operation = TableOperation.InsertOrReplace(item);
            await itemsTable.ExecuteAsync(operation);

            await UploadAsyncById(item.Id, filePath);

            var cachedPath = GetCahcedFilePath(item.Id);
            File.Copy(filePath, cachedPath, true);
            File.SetLastWriteTimeUtc(cachedPath, item.LastModified);

            Debug.WriteLine("### Uplaod " + sw.Elapsed.ToString());
        }

        public async Task<Tuple<StorageItem, StorageItem>> MoveAsync(string sourcePath, string targetPath)
        {
            Debug.WriteLine("*** Move " + sourcePath + " " + targetPath);

            var sourceItem = GetItem(sourcePath);
            if (sourceItem == null) throw CreateNotFoundException(sourcePath);

            var targetItem = GetItem(targetPath);
            var targetDirectory = StorageItemUtil.GetParent(targetPath);
            var taregtFileName = StorageItemUtil.GetFileName(targetPath);

            var dire = GetItem(targetDirectory);
            if (dire == null) throw CreateNotFoundException(targetDirectory);

            var sourceDirectory = StorageItemUtil.GetParent(sourcePath);
            if (targetDirectory == sourceDirectory)
            {
                sourceItem.Name = taregtFileName;
                var updateOperation = TableOperation.Replace(sourceItem);
                itemsTable.Execute(updateOperation);
            }
            else
            {
                targetItem = new StorageItem(dire.Id, sourceItem.Id);
                targetItem.Name = taregtFileName;
                targetItem.Size = sourceItem.Size;
                targetItem.Created = sourceItem.Created;
                targetItem.LastModified = sourceItem.LastModified;
                targetItem.Owner = sourceItem.Owner;
                targetItem.Type = GetDataType(targetPath);

                cache.AddCachedData(targetItem);
                cache.RemoveCachedData(sourceItem);

                var addOperation = TableOperation.InsertOrReplace(targetItem);
                await itemsTable.ExecuteAsync(addOperation);
                var deleteOperation = TableOperation.Delete(sourceItem);
                await itemsTable.ExecuteAsync(deleteOperation);

            }
            return new Tuple<StorageItem, StorageItem>(sourceItem, targetItem);
        }

        public async Task<StorageItem> CopyAsync(string sourcePath, string targetPath)
        {
            Debug.WriteLine("*** Copy " + sourcePath + " " + targetPath);

            var sourceItem = GetItem(sourcePath);
            if (sourceItem == null) throw CreateNotFoundException(sourcePath);

            var targetItem = GetItem(targetPath);
            var targetDirectory = StorageItemUtil.GetParent(targetPath);
            var taregtFileName = StorageItemUtil.GetFileName(targetPath);

            var dire = GetItem(targetDirectory);
            if (dire == null) throw CreateNotFoundException(targetDirectory);
            targetItem = new StorageItem(dire.Id, sourceItem.Id);
            targetItem.Name = taregtFileName;
            targetItem.Size = sourceItem.Size;
            targetItem.Created = sourceItem.Created;
            targetItem.LastModified = sourceItem.LastModified;
            targetItem.Owner = sourceItem.Owner;
            targetItem.Type = GetDataType(targetPath);
            var addOperation = TableOperation.InsertOrReplace(targetItem);
            itemsTable.Execute(addOperation);
            var temp = Path.GetTempFileName();
            try
            {
                CloudBlockBlob source = fileContainer.GetBlockBlobReference(IdToRelativePath(sourceItem.Id));
                CloudBlockBlob target = fileContainer.GetBlockBlobReference(IdToRelativePath(targetItem.Id));

                await target.StartCopyAsync(source);
            }
            finally
            {
                File.Delete(temp);
            }
            cache.AddCachedData(targetItem);
            return targetItem;
        }

        public async Task<StorageItem> DeleteAsync(string path)
        {
            Debug.WriteLine("*** Delete " + path);

            var item = GetItem(path);
            if (item == null) throw CreateNotFoundException(path);
            var fileName = StorageItemUtil.GetFileName(path);
            if (item.IsFile)
            {
                var cachedPath = GetCahcedFilePath(item.Id);
                if (File.Exists(cachedPath)) File.Delete(cachedPath);

                var blob = fileContainer.GetBlockBlobReference(IdToRelativePath(item.Id));
                if (await blob.ExistsAsync()) await blob.DeleteAsync();

                var delteOperation = TableOperation.Delete(item);
                await itemsTable.ExecuteAsync(delteOperation);
            }
            else
            {
                var files = GetItemsByFolderId(item.Id);
                if (files.Count != 0)
                {
                    var ex = new ApplicationException("NotEmptyDirectory");
                    ex.Data["path"] = path;
                    throw ex;
                }
                else
                {
                    var delteOperation = TableOperation.Delete(item);
                    await itemsTable.ExecuteAsync(delteOperation);
                    cache.ClearCachedData(item.Id);
                }
            }
            cache.RemoveCachedData(item);
            return item;
        }

        public async Task DownloadAsync(string path, string filePath)
        {
            Debug.WriteLine("*** Download " + path);
            var item = GetItem(path);
            if (item == null) throw CreateNotFoundException(path);
            var cachedPath = GetCahcedFilePath(item.Id);
            if (File.Exists(cachedPath) && item.LastModified == File.GetLastWriteTimeUtc(cachedPath))
            {
                File.Copy(cachedPath, filePath, true);
                Debug.WriteLine("### Downloaded from cache");
            }
            else
            {
                await DownloadAsyncById(item.Id, filePath);
                File.Copy(filePath, cachedPath, true);
                File.SetLastWriteTimeUtc(cachedPath, item.LastModified);
                Debug.WriteLine("### Downloaded from blob");
            }
        }


        public async Task BackupAsync()
        {
            var tableQuery = new TableQuery<StorageItem>();
            var backupFilter = TableQuery.GenerateFilterConditionForBool("HasBackup", QueryComparisons.NotEqual, true);

            TableContinuationToken continuationToken = null;
            do
            {
                var tableQueryResult =
                    await itemsTable.ExecuteQuerySegmentedAsync(tableQuery.Where(backupFilter), continuationToken);
                continuationToken = tableQueryResult.ContinuationToken;

                foreach (var item in tableQueryResult.Where(x=>x.IsFile))
                {
                    var path = GetRelativePath(item);
                    CloudBlockBlob source = fileContainer.GetBlockBlobReference(IdToRelativePath(item.Id));
                    CloudBlockBlob target = backupContainer.GetBlockBlobReference(path);
                    var context = new OperationContext();
                    context.RequestCompleted += (s,e)=> {
                        if (e.RequestInformation.Exception == null)
                        {
                            if (target.CopyState.Status == CopyStatus.Success)
                            {
                                target.CreateSnapshot();
                                item.HasBackup = true;
                                var updateOperation = TableOperation.Replace(item);
                                itemsTable.Execute(updateOperation);
                            }
                        }
                    };
                    await target.StartCopyAsync(source,null,null,null, context);                    
                }
            } while (continuationToken != null);
        }
        public List<IListBlobItem> ListBackup(string path, bool snapshot)
        {
            var list = new List<IListBlobItem>();
            BlobContinuationToken continuationToken = null;
            do
            {
                var backupListResult = backupContainer.ListBlobsSegmented(path.TrimStart('/'), false, BlobListingDetails.None, null, continuationToken, null, null);
                continuationToken = backupListResult.ContinuationToken;

                foreach (var item in backupListResult.Results.OfType<CloudBlockBlob>())
                {
                    foreach (var snap in backupContainer
                        .ListBlobs(item.Name, true, BlobListingDetails.Snapshots, null, null)
                        .Cast<CloudBlockBlob>())
                    {
                        list.Add(snap);
                    }
                }
                foreach (var item in backupListResult.Results.OfType<CloudBlobDirectory>())
                {
                    list.Add(item);
                }
            } while (continuationToken != null);
            return list;
        }

        public async Task PurgeBackupAsync(TimeSpan[] keepHistory)
        {
            var snapRange = keepHistory.Select(x => new DateTimeOffset(DateTime.UtcNow.Subtract(x))).Concat(new DateTimeOffset[] { DateTimeOffset.MinValue }).ToArray();
            BlobContinuationToken continuationToken = null;
            do
            {
                var backupListResult = await backupContainer.ListBlobsSegmentedAsync(null, true, BlobListingDetails.Snapshots, null,continuationToken, null, null);
                continuationToken = backupListResult.ContinuationToken;

                foreach (var itemGroup in backupListResult.Results.Cast<CloudBlockBlob>().Where(x=>x.IsSnapshot).GroupBy(x=>x.Uri))
                {
                    if (itemGroup.Count() > snapRange.Length)
                    {
                        await PurgeHistory(snapRange, itemGroup.ToList());
                    }
                }
            } while (continuationToken != null);
        }

        private async Task PurgeHistory(DateTimeOffset[] keepHistory, List<CloudBlockBlob> snapshots)
        {
            var toDate = DateTimeOffset.MaxValue;
            foreach (var fromDate in keepHistory)
            {
                var his = snapshots.Where(x => x.SnapshotTime >= fromDate && x.SnapshotTime < toDate).OrderBy(x => x.SnapshotTime);
                if (his.Count() >= 3)
                {
                    foreach(var item in his.Skip(1).Take(his.Count()-2))
                    {
                        await item.DeleteAsync();
                    }
                }
                toDate = fromDate;
            }

        }

        private string IdToRelativePath(string id)
        {
            return id;
        }

        private async Task UploadAsyncById(string id, string filePath)
        {
            var blob = fileContainer.GetBlockBlobReference(IdToRelativePath(id));
            await blob.UploadFromFileAsync(filePath);
        }

        private async Task DownloadAsyncById(string id, string filePath)
        {
            var blob = fileContainer.GetBlockBlobReference(IdToRelativePath(id));
            await blob.DownloadToFileAsync(filePath, FileMode.Create);
        }

        private string GetCurretUser()
        {
            return null;
        }

        private Exception CreateNotFoundException(string path)
        {
            var ex = new ApplicationException("NotFound");
            ex.Data["path"] = path;
            return ex;
        }

        private void AddAsyncTasks(Task item)
        {
            lock(asyncTasks)
            {
                asyncTasks.Add(item);
            }
            item.ContinueWith((x) => {
                lock (asyncTasks)
                {
                    asyncTasks.Remove(item);
                }
            });
        }
        public static void WaitAsyncTasks()
        {
            lock (asyncTasks)
            {
                Task.WaitAll(asyncTasks.ToArray());
            }
        }
    }

}
